use axum::async_trait;
use fred::{
    prelude::{KeysInterface, RedisPool},
    types::Expiration,
};

#[async_trait]
pub trait ICacheService: Sync + Send {
    async fn set(&self, key: &str, value: String) -> Option<String>;

    async fn get(&self, key: &str) -> Option<String>;

    /// set key Expiration in milliseconds.
    async fn set_ex(&self, key: &str, value: String, ex: u64) -> Option<String>;

    async fn del(&self, key: &str) -> Option<u64>;
}

pub struct RedisCache(RedisPool);

impl RedisCache {
    pub fn new(redis: RedisPool) -> Self {
        Self(redis)
    }
}

#[async_trait]
impl ICacheService for RedisCache {
    async fn set(&self, key: &str, value: String) -> Option<String> {
        self.0
            .set(key, value, Some(Expiration::KEEPTTL), None, false)
            .await
            .ok()
    }

    async fn set_ex(&self, key: &str, value: String, ex: u64) -> Option<String> {
        self.0
            .set(key, value, Some(Expiration::PX(ex as i64)), None, false)
            .await
            .ok()
    }

    async fn get(&self, key: &str) -> Option<String> {
        self.0.get(key).await.ok()
    }

    async fn del(&self, key: &str) -> Option<u64> {
        self.0.del(key).await.ok()
    }
}

struct AneExpiry;

impl moka::Expiry<String, (Expiration, String)> for AneExpiry {
    /// Returns the duration of the expiration of the value that was just
    /// created.
    fn expire_after_create(
        &self,
        _key: &String,
        value: &(Expiration, String),
        _current_time: std::time::Instant,
    ) -> Option<std::time::Duration> {
        match value.0 {
            Expiration::EX(t) => Some(std::time::Duration::from_secs(t as u64)),
            Expiration::PX(t) => Some(std::time::Duration::from_micros(t as u64)),
            Expiration::EXAT(t) => Some(std::time::Duration::from_secs(t as u64)),
            Expiration::PXAT(t) => Some(std::time::Duration::from_micros(t as u64)),
            Expiration::KEEPTTL => None,
        }
    }
    fn expire_after_update(
        &self,
        _key: &String,
        value: &(Expiration, String),
        _updated_at: std::time::Instant,
        _duration_until_expiry: Option<std::time::Duration>,
    ) -> Option<std::time::Duration> {
        match value.0 {
            Expiration::EX(t) => Some(std::time::Duration::from_secs(t as u64)),
            Expiration::PX(t) => Some(std::time::Duration::from_micros(t as u64)),
            Expiration::EXAT(t) => Some(std::time::Duration::from_secs(t as u64)),
            Expiration::PXAT(t) => Some(std::time::Duration::from_micros(t as u64)),
            Expiration::KEEPTTL => None,
        }
    }
}

pub struct MokaCache(moka::sync::Cache<String, (Expiration, String)>);

impl MokaCache {
    pub fn new(max_capacity: u64) -> Self {
        Self(
            moka::sync::CacheBuilder::new(max_capacity)
                .time_to_idle(std::time::Duration::from_secs(5 * 60))
                .time_to_live(std::time::Duration::from_secs(30 * 60))
                .expire_after(AneExpiry)
                .build(),
        )
    }
}

#[async_trait]
impl ICacheService for MokaCache {
    async fn set(&self, key: &str, value: String) -> Option<String> {
        let res = self.0.get(key).map(|(_, r)| r);
        self.0.insert(key.to_owned(), (Expiration::KEEPTTL, value));
        res
    }

    async fn get(&self, key: &str) -> Option<String> {
        self.0.get(key).map(|(_, r)| r)
    }

    async fn set_ex(&self, key: &str, value: String, ex: u64) -> Option<String> {
        let res = self.0.get(key).map(|(_, r)| r);
        self.0
            .insert(key.to_owned(), (Expiration::PX(ex as i64), value));
        res
    }

    async fn del(&self, key: &str) -> Option<u64> {
        self.0.remove(key).map(|_| 1)
    }
}
